#!/usr/bin/env python

#   Trolls Outta Luckland v0.0.6
#   Copyright 2009-2010, Gummbum
#
#   This file is part of Trolls Outta Luckland.
#
#   Trolls Outta Luckland is free software: you can redistribute it
#   and/or modify it under the terms of the GNU General Public License
#   as published by the Free Software Foundation, either version 3 of
#   the License, or (at your option) any later version.
#
#   Trolls Outta Luckland is distributed in the hope that it will be
#   useful, but WITHOUT ANY WARRANTY; without even the implied warranty
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with Trolls Outta Luckland.  If not, see
#   <http://www.gnu.org/licenses/>.

"""
"""

__version__ = '0.0.4'
__revision__ = '0.0.6'

###Python imports
from math import sqrt, pow, pi, atan2, sin, cos, degrees, radians
import os
import re
import sys

import pygame
from pygame.locals import Color

try:
    ### Pygame imports
    import pygame
    from pygame.locals import MOUSEBUTTONDOWN, RLEACCEL
    ### Game imports
    import game_globals
except ImportError, e:
    print 'ImportError: %s' % e
    print '    in file "%s"' % os.path.abspath(sys.argv[0])

def split_thousands(s, sep=','): 
    """insert a comma every thousands place"""
    if len(s) <= 3: return s
    return split_thousands(s[:-3], sep) + sep + s[-3:]

def constrain(rect, bounds):
    """adjust rect to keep it completely in bounds"""
    moved = False
    if rect.left < bounds.left:
        rect.left = bounds.left
        moved = True
    if rect.top < bounds.top:
        rect.top = bounds.top
        moved = True
    if rect.right > bounds.right:
        rect.right = bounds.right
        moved = True
    if rect.bottom > bounds.bottom:
        rect.bottom = bounds.bottom
        moved = True
    return moved

def mouse_to_game(pos):
    offsetx = game_globals.game.window._subrect.left
    return pos[0]-offsetx, pos[1]

def game_to_mouse(pos):
    offsetx = game_globals.game.window._subrect.left
    return pos[0]+offsetx, pos[1]

def distance(a, b):
    """calculate the distance between points a and b
    
    Returns distance as a float.
    a and b should be float. a is point x1,y1, b is point x2,y2.
    """
    diffx = a[0] - b[0]
    diffy = a[1] - b[1]
    return sqrt(pow(diffx,2) + pow(diffy,2))


def fill_gradient(surface, color, gradient):
    """fill a surface with a gradient pattern
    
    Pygame recipe: http://www.pygame.org/wiki/GradientCode
    """
    drawing_area = surface.get_rect()
    x1 = drawing_area.left
    x2 = drawing_area.right
    a, b = color, gradient
    y1 = drawing_area.top
    y2 = drawing_area.bottom
    h = y2-y1
    rate = (
        float(b[0]-a[0])/h,
        float(b[1]-a[1])/h,
        float(b[2]-a[2])/h
    )
    fn_line = pygame.draw.line
    for line in range(y1,y2):
        color = (
            min(max(a[0]+(rate[0]*line),0),255),
            min(max(a[1]+(rate[1]*line),0),255),
            min(max(a[2]+(rate[2]*line),0),255)
        )
        fn_line(surface, color, (x1,line), (x2,line))

def _init_fonts():
    """Initializes the shared font cache."""
    global _fonts
    _fonts = dict(
        default = pygame.font.SysFont('Vera', 16),
        default_bold = pygame.font.SysFont('Vera', 16, True),
        gui = pygame.font.SysFont('Vera', 16),
        gui_bold = pygame.font.SysFont('Vera', 16, True),
)
_fonts = {}

def get_font(name='default'):
    """Return the named pygame.font.
    
    The named font is looked up in cached fonts. The font named 'default'
    is always available. See make_font() for adding custom fonts to the
    cache.
    """
    global _fonts
    if len(_fonts.keys()) == 0:
        _init_fonts()
    return _fonts[name]

def get_font_names():
    """Return a list of existing font names."""
    return _fonts.keys()

def import_module(fullname):
    """Dynamically imports a module by name

    fullname is a string in any of these syntaxes:
    1. A file name in the Python path, with or without .py extension.
    2. Relative or absolute path and file, with or without .py extension. If
    path contains os.pathsep, the Python path is not used to locate the file.
    A relative path is relative to the current working directory.
    """
    if fullname.endswith('.py'):
        fullname = fullname[:-3]
    parts = re.split(r'[\\/]+', fullname)
    if len(parts) > 1:
        shortname = parts.pop()
        module_dir = os.pathsep.join(parts)
        sys.path.append(module_dir)
    else:
        shortname = fullname
        sys.path.append(os.getcwd())
    try:
        __import__(shortname)
    except ImportError:
        print "No %s.py found in module path:" % shortname
        for p in sys.path:
            print "   %s" % p
        sys.exit()
    return sys.modules[shortname]

def make_font(name, font_name, font_size, bold=False, italic=False):
    """Return the named pygame.font.
    
    An instance of pygame.font is created and stored in a cache for
    retrieval by get_font(). Initially there is one font named 'default',
    which may be replaced by a custom font via this function if desired.
    Arguments mirror those of pygame.font.SysFont().
    """
    global _fonts
    if len(_fonts.keys()) == 0:
        _init_fonts()
    _fonts[name] = pygame.font.SysFont(font_name, font_size, bold, italic)
    return _fonts[name]

def draw_text(image, text, pos=(0,0), font='default', fg=(254,254,254),
    rect_attr=('top','left')):
    """Render text on the image in the named font.
    
    Text is rendered at pos in foreground color fg. Width and height of
    the rendered text is returned. rect_attr are the position attributes
    of the resulting rect to set equal to pos.
    """
    font = get_font(font)
    if type(fg) is not Color:
        fg = Color(fg)
    font_image = font.render(text, True, fg)
    font_rect = font_image.get_rect()
    for i in range(2):
        setattr(font_rect, rect_attr[i], pos[i])
    image.blit(font_image, font_rect)
    return font.size(text)

def calc_circumference_point(radius, origin, degrees_, to_screen=True):
    x1,y1 = 0,0
    if to_screen:
        degrees_ -= 90
    radians_ = radians(degrees_)
    x = x1 + radius * cos(radians_)
    y = y1 + radius * sin(radians_)
    # translate coordinates to game space
    x += origin[0]
    y += origin[1]
    return x,y

def calc_angle(p1, p2, nose=(0,1), neg_ok=False, to_screen=True):
    x1,y1 = p1
    x2,y2 = p2
    # translate p1 to origin 0,0; translate p2 towards origin by same amount
    x1,y1 = p1
    x2,y2 = p2
    t1 = 0,0
    t2 = x2-x1,y2-y1
    # put the new values back for code readability
    x1,y1 = t1
    x2,y2 = t2
    # base image always "points" in this direction
    x1 -= nose[0]
    y1 -= nose[1]
    if to_screen:
        y1 = -y1
        y2 = -y2
#    deg = degrees(atan2(y2 - y1, x2 - x1))
#    if to_screen:
#        deg -= 90
    deg = degrees(atan2(y1,x1) - atan2(y2,x2))
    # correct negative to positive
    if deg < 0 and not neg_ok:
        deg += 360
    return deg

def calc_angle_orig(p1, p2, nose=(0,1), neg_ok=False, to_screen=True):
    """calculate degrees to feed pygame.transform.rotate
    
    Given an image whose center is p1 with nose pointing up, calculate
    the degrees needed to rotate the image so its nose points towards p2.
    
    p1 and p2 are the image's position and the destination in screen x,y
    coordinates (an iterable type, e.g. list, tuple, vec2d), respectively.
    
    nose is the attitude of the base image (think "missile"):
        (0,1)  nose up
        (0,-1) nose down
        (1,0)  nose left
        (-1,0) nose right
    
    If neg_ok is True, the negative values returned by math.atan2 are not
    translated to positive.
    """
    # translate p1 to origin 0,0; translate p2 towards origin by same amount
    x1,y1 = p1
    x2,y2 = p2
    t1 = 0,0
    t2 = x2-x1,y2-y1
    # put the new values back for code readability
    x1,y1 = t1
    x2,y2 = t2
    # base image always "points" in this direction
    x1 -= nose[0]
    y1 -= nose[1]
    deg = degrees(atan2(y1,x1) - atan2(y2,x2))
    if to_screen:
        deg -= 90
    # correct negative to positive
    if deg < 0 and not neg_ok:
        deg += 360
    return deg

def calc_angle_vec2d(vec1, vec2, nose=(0,1), neg_ok=False):
    """the vec2d equivalent of calc_angle()"""
    t1 = vec1 - vec1
    t2 = vec2 - vec1
    t1.x -= nose[0]
    t1.y -= nose[1]
    deg = degrees(atan2(t1.y,t1.x) - atan2(t2.y,t2.x))
    if deg < 0 and not neg_ok:
        deg += 360
    return deg

def calc_line(p0, p1):
    """use Bresenham's algorithm to calculate a set of closed points forming a line
    
    Returns an array whose elements are the points of the line.
    
    Original calc_line code retrieved from:
    http://en.literateprograms.org/Bresenham's_line_algorithm_(Python)?oldid=16281
    """
    origin = x0,y0 = int(p0[0]),int(p0[1])
    x1,y1 = int(p1[0]),int(p1[1])
    points = []
    steep = abs(y1 - y0) > abs(x1 - x0)
    if steep:
        x0, y0 = y0, x0
        x1, y1 = y1, x1
    if x0 > x1:
        x0, x1 = x1, x0
        y0, y1 = y1, y0
    if y0 < y1:
        ystep = 1
    else:
        ystep = -1
    deltax = x1 - x0
    deltay = abs(y1 - y0)
    error = -deltax / 2
    y = y0
    for x in range(x0, x1 + 1): # add 1 to x1 so the range includes x1
        if steep:
            points.append((y,x))
        else:
            points.append((x,y))
        error = error + deltay
        if error > 0:
            y = y + ystep
            error = error - deltax
    if origin != points[0]:
        points.reverse()
    return points

def calculate_bezier(p, steps=30):
    """calculate a bezier curve from 4 control points
    
    Returns a list of the resulting points.
    
    The function uses the forward differencing algorithm described here: 
    http://www.niksula.cs.hut.fi/~hkankaan/Homepages/bezierfast.html
    
    This code taken from www.pygame.org/wiki/BezierCurve.
    """
    #
    t = 1.0 / steps
    temp = t*t
    #
    f = p[0]
    fd = 3 * (p[1] - p[0]) * t
    fdd_per_2 = 3 * (p[0] - 2 * p[1] + p[2]) * temp
    fddd_per_2 = 3 * (3 * (p[1] - p[2]) + p[3] - p[0]) * temp * t
    #
    fddd = fddd_per_2 + fddd_per_2
    fdd = fdd_per_2 + fdd_per_2
    fddd_per_6 = fddd_per_2 * (1.0 / 3)
    #
    points = []
    for x in range(steps):
        points.append(f)
        f = f + fd + fdd_per_2 + fddd_per_6
        fd = fd + fdd + fddd_per_2
        fdd = fdd + fddd
        fdd_per_2 = fdd_per_2 + fddd_per_2
    points.append(f)
    return points

def plot_curve(p):
    """plot a curved path along one or more sets of control points
    
    p is a one-dimensional array of points in multiples of four: i.e.
    four points per curve.
    
    steps is the number of points desired. steps is weighted by the sum
    distance of each four points in order to yield approximately
    equidistant points.
    
    This code derived from www.pygame.org/wiki/BezierCurve.
    """
    plot = []
    for x in range((len(p)-1)/3):
        slice = p[3*x:3*x+4]
        dist = 0.0
        p0 = slice[0]
        for i in range(1,3):
            dist = max(dist, distance(p0, p[i]))
            p0 = p[i]
        n = 200
        b_points = calculate_bezier(slice, n)
        plot.extend(b_points)
    return plot

def load_image(name, colorkey=None, alpha=False):
    """load an image into memory"""
    if name in _IMAGE_CACHE:
        image = _IMAGE_CACHE[name].copy()
    else:
        try:
            image = pygame.image.load(name)
            _IMAGE_CACHE[name] = image
        except pygame.error, message:
            print 'Cannot load image:', name
            raise SystemExit, message
    if alpha:
        image = image.convert_alpha()
    else:
        image=image.convert()
    if colorkey is not None:
        if colorkey is -1:
            colorkey = image.get_at((0,0))
        image.set_colorkey(colorkey, RLEACCEL)
    return image, image.get_rect()
_IMAGE_CACHE = {}

def wait_event(type=MOUSEBUTTONDOWN):
    while 1:
        e = pygame.event.wait()
        if e.type == type:
            return
